# 挂载 Vue 实例
在引入 Vue 模块和实例化 Vue 对象后,要进行 $mount() 操作。
new Vue({
render: h => h(App)
}).$mount('#app')
$mount 方法主要调用 mountComponent 方法。
# mountComponent
函数参数有:
- vm:Vue 实例。
- el:挂载的 DOM 节点,可以是 ID 选择器或者实际的 DOM 节点。
- hydrating:该参数跟服务端渲染有关。
函数执行逻辑为:
- 判断是否实例化 Vue 对象的时候是否有传 render 函数。没有则手动添加上 render 函数,并且这个 render 函数返回一个空的 VNode。主要源码如下:
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
- 实例化一个渲染 Watcher,该 Watcher 会调用回调函数 updateComponent 方法更新 Dom。主要源码如下:
// 会在初始化和数据更新时执行回调
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
updateComponent 方法主要执行:
- 调用原型上的 _render 函数,返回一个 VNode。
- 调用原型上的 _update 函数,更新 DOM。
主要源码如下:
var updateComponent;
/* istanbul ignore if */
if (config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var vnode = vm._render(); // 生成VNdoe
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating); // 更新DOM
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
- 设置挂载标志 _isMounted 为 true,并调用 mounted 钩子。
接下来看 updateComponent 中的 _render 和 _update 方法。
# _render
该函数在引入 Vue 模块的时候执行 renderMixin 函数的时候挂载在 Vue 原型上。该方法最主要的功能就是生成 VNode。
生成 VNode 最核心的就是调用 $createElement 方法。
function renderMixin(Vue) {
Vue.prototype._render = function() {
vnode = render.call(vm._renderProxy, vm.$createElement)
}
}
$createElement 在 initMixin 初始化方法中的 initRender 中已经定义,而 $createElement 最核心的调用 createElement 方法。
function initRender (vm) {
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
}
createElement 方法内部最终调用 _createElement 方法,主要对函数参数进行处理。
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
# _createElement
主要执行:
- 规范化 children:因为传入的 children 是任意类型的,需要将 children 转化为 VNode 类型的数组。
- 生成并返回 VNode。
规范化 children 主要源码如下:
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
}
规范化 children 主要有两种类型的规范化,主要区别在于 render 函数是编译生成的还是用户手写的。
- SIMPLE_NORMALIZE:调用 simpleNormalizeChildren,调用场景是 render 是编译生成的。翻译注释可知:虽然编译生成的 children 已经是 VNode 类型的数组,但是由于 functional component 函数式组件返回的是一个数组而不是一个根节点,需要对其 children 进行扁平化处理,使其深度只有一层。
// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
function simpleNormalizeChildren (children) {
for (var i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
- ALWAYS_NORMALIZE:调用 normalizeChildren,调用场景是 render 是用户手写的。
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
function normalizeChildren (children) {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
normalizeArrayChildren 的逻辑如下:
- 如果有嵌套逻辑,则递归调用 normalizeArrayChildren;
- 如果是字符串或者数字,则创建文本节点。
规范化 children 之后,生成 VNode,主要有以下逻辑:
- 如果标签名 tag 是字符串类型:
- tag 为内置节点/html 标签名:直接创建 VNode 实例;
- tag 为已经注册的组件名,则调用 createComponent 创建 VNode。
- 组件类型,调用 createComponent 创建 VNode。
# _update
该函数在引入 Vue 模块的时候执行 lifecycleMixin 函数的时候挂载在 Vue 原型上。
function lifecycleMixin(Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
}
_update 的作用就是通过 VNode 生成真实的 DOM 节点并插入到父节点中。
上面代码最主要的逻辑就是调用 __patch__
函数并赋值给 vm.$el。
__patch__
调用逻辑源码:
Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
可见,在浏览器端,调用的是 createPatchFunction 函数,函数参数为:
- nodeOps:封装了 DOM 操作方法
- modules:定义了一些模块的钩子函数的实现
createPatchFunction 函数内部返回了一个 patch 函数,函数为:
function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}
逻辑捋下来,在 __update
中初始化逻辑 vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
最终调用的 createPathFunction 返回的 patch 函数。
# patch
patch 函数内部,略过服务端渲染和判空逻辑,只保留初始化逻辑,代码如下:
function patch (oldVnode, vnode, hydrating, removeOnly) {
var isInitialPatch = false;
var insertedVnodeQueue = [];
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
}
}
patch 函数的实参为:
- oldVnode:vm.$el 挂载元素,在 $mount -> mountComponent 中已赋值
- vnode:_render 函数返回的 Vnode
- hydrating:false,非服务端渲染
- removeOnly:false
将函数先判断是否是实际的 DOM 节点,这里是真实的 DOM 节点,然后通过 emptyNodeAt 创建一个新的 VNode 对象。emptyNodeAt 源码:
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
将挂载点 oldVnode 转化为 VNode 之后,调用 createElm 创建新的 node 节点,生成实际的 DOM 节点。createElm 主要初始化源码为:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
上面代码分析可得,由于挂载元素(vm.$el)是实际的 DOM 元素,故上面的主要执行逻辑为:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
接下来调用平台的方法创建元素,这里创建的元素是挂载元素,然后设置调用 createChildren 创建子元素节点。createChildren 函数源码:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
{
checkDuplicateKeys(children);
}
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
每个 VNode 都有一个 children 属性,用于包裹子元素节点。createChildren 函数通过判断 children 是否是数组列表,是的话通过深度优先遍历递归调用 createElm 方法创建 DOM 节点。
接着调用 invokeCreateHooks 执行所有的 VNode create 钩子,并将 VNode push 到 insertedVnodeQueue 中。invokeCreateHooks 函数源码如下:
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
最后调用 insert(parentElm, vnode.elm, refElm);
将最终的 DOM 插到父节点中。注意,深度遍历递归的顺序是先子后父,故最终会回溯到挂载节点上。insert 源码如下:
function insert (parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}